/**
 * 删除目录问题：	1. 删除目录后会导致无法获取目录下的文件路径，inode 信息也会异常
 * 				2. 这种情况只能用 filename 粗略匹配，丢失了中间目录的路径，只能获取到最上级目录路径和该目录、文件路径
 *				3. 单个文件操作时没有这种情况
 *
 */
package auditd

import (
	"context"
	"dario.cat/mergo"
	"fmt"
	"github.com/google/btree"
	"github.com/nxadm/tail"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	SYSCALL       = "SYSCALL"
	PATH          = "PATH"
	CWD           = "CWD"
	CONFIG_CHANGE = "CONFIG_CHANGE"
	PROCTITLE     = "PROCTITLE"
)

var (
	AuditTree = &AuditBtree{
		BTree: btree.New(32),
		Mutex: new(sync.Mutex),
	}
	auditKeys = &sync.Map{}
)

// Less 该方法用于 btree 的查找和删除操作, 此处绝对路径作为 key
func AddAuditKey(keys ...string) {
	for i := 0; i < len(keys); i++ {
		auditKeys.Store(keys[i], struct{}{})
	}
}

type AuditItem struct {
	Filled    bool   // 数据是否填充完成
	time.Time        // 时间戳
	ID        int    // 序号
	AbsPath   string // 文件绝对路径
	Key       string
	*SyscallField
	*CwdField
	PathFields []*PathField
	*ConfigChange
	*Proctitle

	skipMsg string
}

type SyscallField struct {
	ARCH    string //  ARCH -> x86_64
	Msg     string //  msg -> audit(1717034227.055:8962)
	Success string //  success -> yes
	Ppid    string //  ppid -> 77161
	Comm    string //  comm -> cat
	GID     string //  GID -> yang
	EUID    string //  EUID -> yang
	FSGID   string //  FSGID -> yang
	Egid    string //  egid -> 1000
	UID     string //  UID -> yang
	FSUID   string //  FSUID -> yang
	Items   string //  items -> 1
	Uid     string //  uid -> 1000
	Subj    string //  subj -> ?
	EGID    string //  EGID -> yang
	A3      string //  a3 -> 0
	Suid    string //  suid -> 1000
	Ses     string //  ses -> 2
	Key     string //  key -> audit-test
	Arch    string //  arch -> c000003e
	Exit    string //  exit -> 3
	Pid     string //  pid -> 77307
	Fsgid   string //  fsgid -> 1000
	Tty     string //  tty -> pts2
	SYSCALL string //  SYSCALL -> openat
	AUID    string //  AUID -> yang
	SUID    string //  SUID -> yang
	SGID    string //  SGID -> yang
	Type    string //  type -> SYSCALL
	Euid    string //  euid -> 1000
	Fsuid   string //  fsuid -> 1000
	Syscall string //  syscall -> 257
	A0      string //  a0 -> ffffff9c
	A1      string //  a1 -> 7ffcffdae23f
	A2      string //  a2 -> 0
	Auid    string //  auid -> 1000
	Gid     string //  gid -> 1000
	Sgid    string //  sgid -> 1000
	Exe     string //  exe -> /usr/bin/cat
}

type CwdField struct {
	Type string // 0 = type -> CWD
	Msg  string // 1 = msg -> audit(1717034227.055:8962)
	Cwd  string // 2 = cwd -> /home/yang/audit-test
}

type PathField struct {
	Name     string // 0 = name -> hi
	Mode     string // 1 = mode -> 0100664
	Nametype string // 2 = nametype -> NORMAL
	Cap_fi   string // 3 = cap_fi -> 0
	Cap_fe   string // 4 = cap_fe -> 0
	Inode    string // 5 = inode -> 2363965
	Dev      string // 6 = dev -> 08:03
	Ouid     string // 7 = ouid -> 1000
	Cap_fp   string // 8 = cap_fp -> 0
	OGID     string // 9 = OGID -> yang
	Msg      string // 10 = msg -> audit(1717034227.055:8962)
	Ogid     string // 11 = ogid -> 1000
	Rdev     string // 12 = rdev -> 00:00
	Cap_fver string // 13 = cap_fver -> 0
	//0OUID          string // 14 = 0OUID -> yang
	Type        string // 15 = type -> PATH
	Item        string // 16 = Item -> 0
	Cap_frootid string // 17 = cap_frootid ->

	// centOS 多出来的字段
	Obj     string // obj -> unconfined_u:object_r:user_home_t:s0
	Objtype string // objtype -> NORMAL
}

type ConfigChange struct {
	Type string // type=CONFIG_CHANGE
	Msg  string // msg=audit(1720609352.176:276):
	Auid string // auid=1000
	Ses  string // ses=2
	Subj string // subj=?
	Op   string // op=add_rule
	Key  string // key="system-reboot"
	List string // list=4
	Res  string // res=1
}

type Proctitle struct {
	Type      string // type=PROCTITLE
	Msg       string // msg=audit(1720609352.176:276):
	Proctitle string // proctitle=2F7362696E2F617564697463746C002D52002F6574632F61756469742F61756469742E72756C6573
}

// TailAuditLog
func TailAuditLog(ctx context.Context) error {
	defer AuditTree.Clear(false)

	tailFile, err := tail.TailFile("/var/log/audit/audit.log",
		tail.Config{Follow: true, ReOpen: true, Logger: log.New(os.Stdout, "==audit==", log.Lshortfile)})
	if err != nil {
		return err
	}
	ticker := time.NewTicker(time.Millisecond)
	defer ticker.Stop()
	var (
		flag    = 0 // 标记是否有新的审计日志产生
		tm      time.Time
		id      int
		ok      bool
		key     interface{}
		skipMsg = ""

		item  = &AuditItem{}
		added = false
	)
	for {
	start:
		select {
		case <-ctx.Done():
			return nil
		case line := <-tailFile.Lines:
			flag++
			if len(line.Text) < 1 {
				goto start
			}
			fieldMap := splitFields(line.Text)
			if fieldMap["msg"].(string) == skipMsg {
				goto start
			}

			tm, id, err = getTimeAndId(fieldMap)
			if err != nil {
				log.Fatal(err)
			}
			if !item.Equal(tm, id) {
				if !added {
					if err = item.postHandle(); err != nil {
						log.Println(err)
					}
					if len(item.Key) > 0 { // skip logic based skip msg & auditKeys
						AuditTree.ReplaceOrInsert(item.Clone())
					} else {
						item.Reset()
					}
				}
				added = false

				item = &AuditItem{
					Time: tm,
					ID:   id,
				}
			}

			key = fieldMap["key"]
			if key != nil && len(key.(string)) > 0 && !strings.Contains(key.(string), "null") {
				if _, ok = auditKeys.Load(key); ok {
					item.Key = key.(string)
				} else {
					skipMsg = fieldMap["msg"].(string)
					item.skipMsg = skipMsg // skip logic based skip msg & auditKeys
					goto start
				}
			}

			if err = item.handleFields(fieldMap); err != nil {
				log.Println(err)
			}
		case <-ticker.C:
			// 处理 tail 读完最后一行后阻塞的情况
			if flag == 0 {
				if err = item.postHandle(); err != nil {
					log.Println(err)
				}
				if len(item.Key) > 0 { // skip logic based skip msg & auditKeys
					added = true
					AuditTree.ReplaceOrInsert(item.Clone())
				} else {
					item.Reset()
				}
			} else {
				flag = 0
			}
		}
	}

	return nil
}

// Less 该方法用于 btree 的查找和删除操作, 此处绝对路径作为 key
func (a *AuditItem) Clone() *AuditItem {
	return &AuditItem{
		Filled:       a.Filled,
		Time:         a.Time,
		ID:           a.ID,
		AbsPath:      a.AbsPath,
		Key:          a.Key,
		SyscallField: a.SyscallField,
		CwdField:     a.CwdField,
		PathFields:   a.PathFields,
		ConfigChange: a.ConfigChange,
		Proctitle:    a.Proctitle,
	}
}

// Less 该方法用于 btree 的查找和删除操作, 此处绝对路径作为 key
func (a *AuditItem) Reset() {
	a.Filled = false
	a.Time = time.Time{}
	a.ID = 0
	a.AbsPath = ""
	a.Key = ""
	a.SyscallField = nil
	a.CwdField = nil
	a.PathFields = nil
	a.skipMsg = ""
}

// Less 该方法用于 btree 的查找和删除操作, 此处绝对路径作为 key
func (a *AuditItem) Less(than btree.Item) bool {
	if than == nil {
		return false
	}
	th, ok := than.(*AuditItem)
	if !ok {
		return false
	}
	//return strings.Compare(a.AbsPath, th.AbsPath) < 0
	return a.Time.Before(th.Time) || a.Time.Equal(th.Time) && a.ID < th.ID
}

func (a *AuditItem) Equal(tm time.Time, id int) bool {
	return a.Time.Equal(tm) && a.ID == id
}

// postHandle 所有信息提取完成后的后期处理
func (a *AuditItem) postHandle() (err error) {
	defer func() {
		if err == nil {
			a.Filled = true
		}
	}()

	if a.CwdField != nil {
		a.AbsPath = a.CwdField.Cwd
	}

	var (
		relPths []string
		pth     string
	)

	for i := 0; i < len(a.PathFields); i++ {
		pth = a.PathFields[i].Name
		if filepath.IsAbs(pth) || strings.HasPrefix(pth, "/") {
			pth, err = filepath.Rel(a.AbsPath, pth)
			if err != nil {
				return err
			}
		}
		relPths = append(relPths, pth)
	}
	pth = ""
	for _, v := range relPths {
		if strings.HasPrefix(v, pth) {
			pth = v
		} else {
			pth = filepath.Join(pth, v)
		}
	}
	a.AbsPath = filepath.Join(a.AbsPath, pth)

	return nil
}

// handleFields 解析 fields 信息，并填充到对应的字段中
func (a *AuditItem) handleFields(fieldMap map[string]interface{}) (err error) {
	switch strings.ToUpper(fieldMap["type"].(string)) {
	case SYSCALL:
		field := &SyscallField{}
		if err = mergo.Map(field, fieldMap); err != nil {
			return err
		}
		a.SyscallField = field
	case CWD:
		field := &CwdField{}
		if err = mergo.Map(field, fieldMap); err != nil {
			return err
		}
		a.CwdField = field
	case PATH:
		field := &PathField{}
		if err = mergo.Map(field, fieldMap); err != nil {
			return err
		}
		a.PathFields = append(a.PathFields, field)
	case CONFIG_CHANGE:
		field := &ConfigChange{}
		if err = mergo.Map(field, fieldMap); err != nil {
			return err
		}
		a.ConfigChange = field
	case PROCTITLE:
		field := &Proctitle{}
		if err = mergo.Map(field, fieldMap); err != nil {
			return err
		}
		a.Proctitle = field
	}
	return nil
}

func getTimeAndId(filedMap map[string]interface{}) (time.Time, int, error) {
	msgVal, ok := filedMap["msg"]
	if !ok || msgVal != nil && len(msgVal.(string)) < 1 {
		return time.UnixMicro(0), 0, fmt.Errorf("")
	}
	msg := msgVal.(string)
	// audit(1717034537.722:8985)
	timestr, nsStr, idStr := msg[6:16], msg[17:20], msg[21:len(msg)-1]
	id, err := strconv.Atoi(idStr)
	if err != nil {
		return time.UnixMicro(0), 0, err
	}
	timestamp, err := strconv.ParseInt(timestr, 10, 64)
	if err != nil {
		return time.UnixMicro(0), 0, err
	}
	ns, err := strconv.ParseInt(nsStr, 10, 64)
	if err != nil {
		return time.UnixMicro(0), 0, err
	}
	t := time.Unix(timestamp, ns)
	loc, _ := time.LoadLocation("Asia/Shanghai")
	t = t.In(loc)

	return t, id, nil
}

func splitRaws(log string) []string {
	return strings.Split(log, "\n")
}

var logReg = regexp.MustCompile("[-\\w]+=")

func splitFields(raw string) map[string]interface{} {
	// remove "Group Separator" (组分隔符)，
	raw = strings.Replace(raw, string(rune(29)), "", -1)
	keys := logReg.FindAllString(raw, -1)
	values := logReg.Split(raw, -1)

	res := make(map[string]interface{}, len(keys))
	key, val := "", ""
	var tmp interface{}
	ok := false
	for i := 0; i < len(keys); i++ {
		key = keys[i]
		key = strings.ReplaceAll(key, "=", "")
		key = strings.ReplaceAll(key, "\"", "")
		key = strings.ReplaceAll(key, "'", "")
		key = strings.TrimSpace(key)

		if tmp, ok = res[key]; ok && tmp != nil && len(tmp.(string)) > 0 {
			continue
		}

		val = values[i+1]
		val = strings.ReplaceAll(val, "\"", "")
		val = strings.ReplaceAll(val, "'", "")
		val = strings.TrimSpace(val)
		val = strings.Trim(val, ":")

		res[key] = val
	}
	return res
}
